<!DOCTYPE html>
<html lang="zh-cn">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div id="app">
    <h1>foo：{{ foo }}</h1>
    <h2>bar：{{ bar }}</h2>
    <h2>计算属性fooBar：{{ fooBar }}</h2>
    <h2>计算属性barFoo：{{ barFoo }}</h2>
    <input type="text" v-model="foo">
    <br>
    <br>
    <input type="text" v-model="bar">
    <br>
    <br>
    <button @click="clickHandler">来,绑定一个事件</button>
</div>

<script>
    const Dep = class Dep {
        constructor() {
            this.subs = []
        }

        // 添加订阅者
        add(sub) {
            this.subs.push(sub)
        }

        // 通知
        notify() {
            this.subs.forEach(sub => sub.update())
        }
    }

    // 存入当前的 Watcher 实例
    Dep.target = null

    // 订阅者-触发 dom 更新
    class Watcher {
        constructor(vm, key, callback) {
            this.vm = vm
            this.key = key
            this.callback = callback
            Dep.target = this
            // 此处会触发 get（ps：这个 value 是 data对象里的 “旧” 值）
            // this.value 感觉是个工具人，难道只是为了触发 get
            this.value = vm[key]
            Dep.target = null
        }

        update() {
            // update 在 set 中调用，所以获取到的是 input 实时输入的值
            const newValue = this.vm[this.key]
            this.callback(newValue)
        }
    }

    // 侦听 data 里的属性 get/set
    function observer(vm) {
        const dep = new Dep()
        const {_computedDep} = vm
        vm.$depKeys.forEach(key => {
            let value = vm[key]
            Object.defineProperty(vm, key, {
                enumerable: true,
                configurable: true,
                get() {
                    if (Dep.target) {
                        // 添加订阅者-Watcher实例
                        dep.add(Dep.target)
                    }
                    return value
                },
                set(newVal) {
                    value = newVal
                    // （ps：可以在此处根据 key 值通知对应的 Watcher 进行更新）
                    dep.notify()

                    Object.keys(_computedDep).forEach(computedDep => {
                        // 在 set 中匹配对应的依赖项更新对应的计算属性
                        if (key === computedDep) {
                            for (let getter in _computedDep[key]) {
                                vm[getter] = _computedDep[key][getter].call(vm)
                            }
                        }
                    })
                }
            })
        })
    }

    // 编译模板
    class Compile {
        constructor(vm) {
            this.vm = vm
            this._init()
        }

        _init() {
            // 搬来 Vue 中匹配 {{ 插值 }} 的正则
            const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/;
            // 获取主容器
            const container = this.vm.$el
            // 创建虚拟节点
            const fragment = document.createDocumentFragment()
            // 只取元素节点
            let firstChild = container.firstElementChild
            while (firstChild) {
                // 这里 append 一个，container 就会少一个 子元素，若没有子元素则会返回 null
                fragment.appendChild(firstChild)
                firstChild = container.firstElementChild
            }
            fragment.childNodes.forEach(node => {
                // 将属性节点(ArrayLike)转换为数组
                [...node.attributes].forEach(attr => {
                    // 匹配 v-model 指令
                    if (attr.name === 'v-model') {
                        const key = attr.value
                        node.value = this.vm[key]

                        new Watcher(this.vm, key, val => {
                            node.value = val
                        })

                        node.addEventListener('input', e => {
                            // input 事件触发 set 方法，并通知 Watcher 实例操作变更dom
                            this.vm[key] = e.target.value
                        })
                    }
                    // 匹配 @click 绑定点击事件
                    if (attr.name === '@click') {
                        // 使用 bind 将此函数内部 this 改为 Vue实例
                        node.addEventListener('click', this.vm[attr.value].bind(this.vm))
                    }
                })

                // 匹配双花括号插值(textContent取赋值 比 innerText 好一点)
                if (node.textContent && defaultTagRE.test(node.textContent)) {
                    console.log(node.textContent);
                    const key = RegExp.$1.trim()
                    // 替换 {{}} 后的文本，用于初始化页面
                    const replaceTextContent = node.textContent.replace(defaultTagRE, this.vm[key])
                    // 移除 {{}} 后的文本，用于响应性更新
                    const removeMustache = node.textContent.replace(defaultTagRE, '')
                    node.textContent = replaceTextContent

                    new Watcher(this.vm, key, val => {
                        node.textContent = removeMustache + val
                    })
                }
            })

            // 将 虚拟节点 添加到主容器中(这里可以将虚拟节点理解为 Vue 中的 template 标签,只起到一个包裹作用不会存在真实标签)
            this.vm.$el.appendChild(fragment)
            // 此处定义 mounted 生命周期
            typeof this.vm.$options.mounted === 'function' && this.vm.$options.mounted.call(this.vm)
        }
    }

    // Vue类
    class Vue {
        constructor(options) {
            this.$options = options
            this.$el = document.querySelector(options.el)
            // 缓存data中的key,用来数据劫持
            // (ps: Vue 将 options.data 中的属性挂在 Vue实例 上, Object.defineProperty 劫持的其实是 Vue实例 上的属性, options.data 里的数据初始化之后应该用处不大)
            this.$depKeys = Object.keys({...options.data(), ...options.computed})
            // 计算属性的依赖数据
            this._computedDep = {}
            this._addProperty(options)
            this._getComputedDep(options.computed)
            this._init()
        }

        _init() {
            observer(this)
            new Compile(this)
        }

        // 获取计算属性依赖
        _getComputedDep(computed) {
            Object.keys(computed).forEach(key => {
                const computedFn = computed[key]
                const computedDeps = this._getDep(computedFn.toString())

                computedDeps.forEach(dep => {
                    if (!this._computedDep[dep]) {
                        this._computedDep[dep] = {
                            [key]: computed[key]
                        }
                    } else {
                        Object.assign(this._computedDep[dep], {
                            [key]: computed[key]
                        })
                    }
                })
            })
        }

        _getDep(fnStr) {
            const NOT_REQUIRED = ['(', ')', '{', '}', '+', '*', '/', '\'']
            return fnStr.replace(/[\r\n ]/g, '')
                .split('')
                .filter(item => !NOT_REQUIRED.includes(item))
                .join('')
                .split('return')[1]
                .split('this.')
                .filter(Boolean)
        }

        // 将 data 和 methods 中的值注入Vue实例中(实现在方法或生命周期等能直接用 this[key] 来取值)
        _addProperty(options) {
            const {computed, data, methods} = options
            const _computed = {}
            Object.keys(computed).forEach(key => {
                // 这里将 this 替换成 data() 是初始化计算属性的值
                _computed[key] = computed[key].call(data())
            })

            const allData = {...data(), ...methods, ..._computed}
            Object.keys(allData).forEach(key => {
                this[key] = allData[key]
            })
        }
    }

    const vm = new Vue({
        el: '#app',
        data() {
            return {
                foo: 'hello',
                bar: 'world'
            }
        },
        computed: {
            // 需要再 this.foo 和 this.bar 的 set 中改变 fooBar 的值（如何自动收集依赖？？）
            fooBar() {
                return this.foo + ' ' + this.bar
            },
            barFoo() {
                return this.bar + ' ' + this.foo
            }
        },
        mounted() {
            console.log(document.querySelector('h1'));
            console.log(this.foo);
        },
        methods: {
            clickHandler() {
                this.foo = 'hello'
                this.bar = 'world'
                console.log('实现一个简单事件!')
                console.log(this)
            }
        }
    })
</script>
</body>
</html>
